﻿#region License Info

//Component of Cronos Package, http://www.codeplex.com/cronos
//Copyright (C) 2009 Anthony Brockwell

//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using MathNet.Numerics.LinearAlgebra;
using PdfSharp.Drawing;

namespace ExtraControlLibrary
{
    public partial class XYPlotBox : PartitionedBlankGraph
    {
        #region Delegates

        public delegate void OnRegionSelect(object sender, RegionSelectArgs e);

        public delegate void OnScrolledEventHandler(object sender, EventArgs e);

        #endregion

        private List<SingleSeriesInfo> seriesInfo;

        /// <summary>
        /// series[] is an array of lists of points.  Each point is represented
        /// by a 2-dimensional vector (x and y coordinates).
        /// </summary>
        private Matrix xyData;

        /// <summary>
        /// Constructor: creates blank (no data) plot.
        /// </summary>
        public XYPlotBox()
        {
            xyData = null;
            lineMode = false;
            InitializeComponent();
        }

        public event OnRegionSelect RegionSelectEvent;

        protected virtual void OnRegionSelectEvent(RegionSelectArgs rsa)
        {
            if (RegionSelectEvent != null)
                RegionSelectEvent(this, rsa);
        }

        public int NumSeries()
        {
            if (xyData == null)
                return 0;
            return 1;
        }

        /// <summary>
        /// Empties the plot box, readies it for new AppendToSeries calls.
        /// </summary>
        public void Clear(bool redraw)
        {
            xyData = null;
            if (redraw)
                Refresh();
        }

        /// <summary>
        /// sets the data that is plotted
        /// </summary>
        public void SetMatrix(Matrix xyData)
        {
            this.xyData = xyData;
            ScanData();
        }

        /// <summary>
        /// This function goes through, evaluates max and min in each coordinate,
        /// and finds some "nice" units to subdivide the ranges into.
        /// Results are stored in member variables.
        /// </summary>
        protected void ScanData()
        {
            if (NumSeries() == 0)
                return;

            seriesInfo = new List<SingleSeriesInfo>(2);
            for (int i = 0; i < 2; ++i)
                seriesInfo.Add(new SingleSeriesInfo());
            for (int i = 0; i < 2; ++i)
                for (int j = 0; j < xyData.RowCount; ++j)
                    seriesInfo[i].Update(xyData[j, i]);

            for (int i = 0; i < 2; ++i)
                seriesInfo[i].ComputeRecommendedUnits();
        }

        protected void PutPointMarker(Graphics g, Pen pen, int pixelX, int pixelY)
        {
            if (markerSize > 0)
                g.DrawRectangle(pen, pixelX - markerSize, pixelY - markerSize, markerSize*2, markerSize*2);
        }

        protected void PutPointMarker(XGraphics g, Pen pen, double pixelX, double pixelY)
        {
            if (markerSize > 0)
                g.DrawRectangle(pen, pixelX - markerSize, pixelY - markerSize, markerSize * 2, markerSize * 2);
        }

        protected void DoXAxisLabeling(Object graphicsObject, Font labelFont)
        {
            // sf.Alignment = StringAlignment.Center;
            double x0 = LeftXValue(), x1 = RightXValue();

            // First create the x-axis value labels
            var xaxisvals = new List<double>();
            List<string> xaxisvallabels;

            for (double tx = seriesInfo[0].ystart; tx <= seriesInfo[0].yend; tx += seriesInfo[0].yunits)
                if ((tx >= x0) && (tx <= x1))
                    xaxisvals.Add(tx - (timeIsRelative ? x0 : 0));

            // Each label should be different, if not we need finer detail,
            // so we use a do while loop below.

            int detaillevel = 1;
            bool doubleup;
            do
            {
                xaxisvallabels = new List<string>();
                doubleup = false;

                string formatstring = "{0:0.";
                for (int i = 0; i < detaillevel; ++i)
                    formatstring += "0";
                formatstring += "e+0}";

                foreach (double xval in xaxisvals)
                {
                    string lbl = String.Format(formatstring, xval);
                    if (xaxisvallabels.Contains(lbl))
                        doubleup = true;
                    xaxisvallabels.Add(lbl);
                }
                if (doubleup)
                {
                    // increase the fineness
                    ++detaillevel;
                    // if exceeded max., break out of loop
                    if (detaillevel > 4)
                        break;
                }
            } while (doubleup);


            // Draw all the x-axis labels making sure text doesn't overlap
            DrawAllXLabels(graphicsObject, labelFont, xaxisvals, xaxisvallabels, xAxisLabel);
        }


        protected void DoYAxisLabeling(object graphicsObject, Font labelFont, double y0, double y1, double yunits)
        {
            using (var sf = new StringFormat())
            {
                sf.Alignment = StringAlignment.Center;

                // First create the value labels
                var yaxisvals = new List<double>();
                List<string> yaxisvallabels;

                for (double ty = y0; ty <= y1; ty += yunits)
                    yaxisvals.Add(ty);

                // Each label should be different, if not we need finer detail,
                // so we use a do while loop below.

                int detaillevel = 1;
                bool doubleup;
                do
                {
                    yaxisvallabels = new List<string>();
                    doubleup = false;

                    // now build string out of y-value to appropriate level of detail
                    if (!TickMode)
                    {
                        string formatstring;
                        if (!TickMode)
                        {
                            formatstring = "{0:0.";
                            for (int i = 0; i < detaillevel; ++i)
                                formatstring += "0";
                            formatstring += "}";

                            foreach (double yval in yaxisvals)
                            {
                                string lbl = String.Format(formatstring, yval);
                                if (yaxisvallabels.Contains(lbl))
                                    doubleup = true;
                                yaxisvallabels.Add(lbl);
                            }
                        }
                        else
                        {
                            formatstring = "{0:##.0";
                            for (int i = 0; i < detaillevel; ++i)
                                formatstring += "0";
                            formatstring += "}";

                            foreach (double yval in yaxisvals)
                            {
                                double yvalInt = Math.Floor(yval);
                                double yvalFrac = yval - yvalInt;
                                string lbl = yvalInt + "'" + String.Format(formatstring, TickResolution*yvalFrac);
                                yaxisvallabels.Add(lbl);
                            }
                        }
                    }
                    else
                    {
                        foreach (double yval in yaxisvals)
                        {
                            var intpart = (int) Math.Floor(yval);
                            double ticknum = (yval - intpart)*TickResolution;
                            string lbl = intpart + "'" + ticknum;
                            if (yaxisvallabels.Contains(lbl))
                                doubleup = true;
                            yaxisvallabels.Add(lbl);
                        }
                    }

                    if (doubleup)
                    {
                        // increase the fineness
                        ++detaillevel;

                        // if exceeded max., give up and break out of loop
                        if (detaillevel > 5)
                            break;
                    }
                } while (doubleup);


                DrawAllYLabels(graphicsObject, labelFont, yaxisvals, yaxisvallabels, yAxisLabel, y0, y1);
            }
        }

        public void RenderToXGraphics(XGraphics g, int titleFontSize, int labelFontSize)
        {
            // Now draw things.
            using (var title_font = new Font("Arial", titleFontSize, FontStyle.Bold, GraphicsUnit.World))
            using (var label_font = new Font("Arial", labelFontSize, FontStyle.Regular, GraphicsUnit.World))
            {
                DrawTitle(title_font, g);

                // Return if nothing to plot
                if ((xyData == null) || (seriesInfo[0].yRange() == 0) || (seriesInfo[1].yRange() == 0))
                    return;

                double plotX0 = LeftXValue();
                double plotX1 = RightXValue();

                double plotY0 = seriesInfo[1].labelstart;
                double plotY1 = seriesInfo[1].labelend;
                double plotYUnits = seriesInfo[1].yunits;

                // then all the x-axis labeling
                DoXAxisLabeling(g, label_font);

                // Draw the y-axis values
                DoYAxisLabeling(g, label_font, plotY0, plotY1, plotYUnits);

                // Now restrict drawing range to graph area
                var clipregion = new Rectangle(GraphRectangle.Left + 1, GraphRectangle.Top + 1,
                                               GraphRectangle.Width, GraphRectangle.Height);

                // Then put in the gray background lines
                for (double tx = seriesInfo[0].labelstart;
                     tx <= seriesInfo[0].labelend;
                     tx += seriesInfo[0].yunits)
                {
                    float xpix = MaptoXPosition(tx);
                    if (xpix >= clipregion.Left && xpix <= clipregion.Right)
                        g.DrawLine(Pens.LightGray, xpix, GraphRectangle.Top, xpix, GraphRectangle.Bottom);
                }

                for (double ty = seriesInfo[1].labelstart;
                     ty <= seriesInfo[1].labelend;
                     ty += seriesInfo[1].yunits)
                {
                    float ypix = MaptoYPosition(ty);
                    if (ypix >= clipregion.Top && ypix <= clipregion.Bottom)
                        g.DrawLine(Pens.LightGray, GraphRectangle.Left, ypix, GraphRectangle.Right, ypix);
                }

                // And draw frame
                DrawGraphFrame(g);

                var penlist = new Pen[5];
                penlist[0] = new Pen(Brushes.Blue, (float) 0.5);
                penlist[1] = new Pen(Brushes.Red, (float) 0.5);
                penlist[2] = new Pen(Brushes.Green, (float) 0.5);
                penlist[3] = new Pen(Brushes.Black, (float) 0.5);
                penlist[4] = new Pen(Brushes.LightGray, (float) 0.5);

                // DRAW THE DATA.
                Pen temppen = penlist[0];

                // now get indices of points falling in the appropriate x-range
                int i0 = 0;
                int i1 = xyData.RowCount;
                int numSubPoints = i1 - i0;
                double[] xpixels = new double[numSubPoints], ypixels = new double[numSubPoints];
                var drawIndices = new List<int>(2000);

                // now compute all the x and y pixel coordinates
                for (int i = i0; i < i1; ++i)
                    xpixels[i - i0] =
                        ((xyData[i, 0] - plotX0)/(plotX1 - plotX0)*
                         GraphRectangle.Width) + GraphRectangle.Left;

                double curXPixel = xpixels[0];
                double firstPoint = xyData[0, 1];
                double pixelMinY = firstPoint;
                double pixelMaxY = pixelMinY;
                int argMin = i0;
                int argMax = i0;
                ypixels[0] = (-(firstPoint - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                             GraphRectangle.Bottom;

                for (int i = i0 + 1; i < i1; ++i)
                {
                    double yval = xyData[i, 1];
                    ypixels[i - i0] = (-(yval - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                      GraphRectangle.Bottom;
                    if (xpixels[i - i0] != curXPixel)
                    {
                        // it's a new pixel, so just summarize the last block and begin the new
                        drawIndices.Add(argMin - i0);
                        if (argMax != argMin)
                            drawIndices.Add(argMax - i0);
                        argMin = i;
                        argMax = i;
                        curXPixel = xpixels[i - i0];
                        pixelMinY = yval;
                        pixelMaxY = yval;
                    }
                    else
                    {
                        // scan for min and max within the pixel
                        if (yval > pixelMaxY)
                        {
                            pixelMaxY = yval;
                            argMax = i;
                        }
                        if (yval < pixelMinY)
                        {
                            pixelMinY = yval;
                            argMin = i;
                        }
                    }
                }
                drawIndices.Add(argMin);
                if (argMax != argMin - i0)
                    drawIndices.Add(argMax - i0);


                // draw the lines
                if (lineMode)
                    for (int j = 0; j < drawIndices.Count - 1; ++j)
                    {
                        int idx1 = drawIndices[j];
                        int idx2 = drawIndices[j + 1];
                        double y1 = ypixels[idx1];
                        double y2 = ypixels[idx2];
                        if ((y1 < 0) || (y1 > 2500))
                            y1 = 0;
                        if ((y2 < 0) || (y2 > 2500))
                            y2 = 0;
                        if (StepMode)
                        {
                            g.DrawLine(temppen, xpixels[idx1], y1, xpixels[idx2], y1);
                            g.DrawLine(temppen, xpixels[idx2], y1, xpixels[idx2], y2);
                        }
                        else
                            g.DrawLine(temppen, xpixels[idx1], ypixels[idx1], xpixels[idx2],
                                       ypixels[idx2]);
                    }

                // we put down the point markers in any case
                for (int j = 0; j < drawIndices.Count; ++j)
                {
                    int idx1 = drawIndices[j];
                    double y1 = ypixels[idx1];
                    PutPointMarker(g, temppen, xpixels[idx1], y1);
                }
            }
        }

        public void RenderToGraphics(Graphics g, int titleFontSize, int labelFontSize)
        {
            // Now draw things.
            using (var title_font = new Font("Arial", titleFontSize, FontStyle.Bold))
            using (var label_font = new Font("Arial", labelFontSize))
            {
                DrawTitle(title_font, g);
                ClearGraphArea(g);

                // Return if nothing to plot
                if ((xyData == null) || (seriesInfo[0].yRange() == 0) || (seriesInfo[1].yRange() == 0))
                    return;

                double plotX0 = LeftXValue();
                double plotX1 = RightXValue();

                double plotY0 = seriesInfo[1].labelstart;
                double plotY1 = seriesInfo[1].labelend;
                double plotYUnits = seriesInfo[1].yunits;

                // then all the x-axis labeling
                DoXAxisLabeling(g, label_font);

                // Draw the y-axis values
                DoYAxisLabeling(g, label_font, plotY0, plotY1, plotYUnits);

                // Now restrict drawing range to graph area
                var clipregion =
                    new Rectangle(GraphRectangle.Left + 1, GraphRectangle.Top + 1, GraphRectangle.Width,
                                  GraphRectangle.Height);
                using (var region = new Region(clipregion))
                {
                    g.Clip = region;

                    // Then put in the gray background lines
                    for (double tx = seriesInfo[0].labelstart;
                         tx <= seriesInfo[0].labelend;
                         tx += seriesInfo[0].yunits)
                    {
                        float xpix = MaptoXPosition(tx);
                        if (xpix >= clipregion.Left && xpix <= clipregion.Right)
                            g.DrawLine(Pens.LightGray, xpix, GraphRectangle.Top, xpix, GraphRectangle.Bottom);
                    }

                    for (double ty = seriesInfo[1].labelstart;
                         ty <= seriesInfo[1].labelend;
                         ty += seriesInfo[1].yunits)
                    {
                        float ypix = MaptoYPosition(ty);
                        if (ypix >= clipregion.Top && ypix <= clipregion.Bottom)
                            g.DrawLine(Pens.LightGray, GraphRectangle.Left, ypix, GraphRectangle.Right, ypix);
                    }

                    // and draw frame
                    DrawGraphFrame(g);

                    var penlist = new Pen[5];
                    penlist[0] = Pens.Blue;
                    penlist[1] = Pens.Red;
                    penlist[2] = Pens.Green;
                    penlist[3] = Pens.Black;
                    penlist[4] = Pens.LightGray; // last one is background in shadowmode


                    // DRAW THE DATA.


                    Pen temppen = penlist[0];

                    // now get indices of points falling in the appropriate x-range
                    int i0 = 0;
                    int i1 = xyData.RowCount;
                    int numSubPoints = i1 - i0;
                    double[] xpixels = new double[numSubPoints], ypixels = new double[numSubPoints];
                    var drawIndices = new List<int>(2000);

                    // now compute all the x and y pixel coordinates
                    for (int i = i0; i < i1; ++i)
                        xpixels[i - i0] =
                            ((xyData[i, 0] - plotX0)/(plotX1 - plotX0)*
                             GraphRectangle.Width) + GraphRectangle.Left;

                    double curXPixel = xpixels[0];
                    double firstPoint = xyData[0, 1];
                    double pixelMinY = firstPoint;
                    double pixelMaxY = pixelMinY;
                    int argMin = i0;
                    int argMax = i0;
                    ypixels[0] = (-(firstPoint - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                 GraphRectangle.Bottom;

                    for (int i = i0 + 1; i < i1; ++i)
                    {
                        double yval = xyData[i, 1];
                        ypixels[i - i0] = (-(yval - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                          GraphRectangle.Bottom;
                        if (xpixels[i - i0] != curXPixel)
                        {
                            // it's a new pixel, so just summarize the last block and begin the new
                            drawIndices.Add(argMin - i0);
                            if (argMax != argMin)
                                drawIndices.Add(argMax - i0);
                            argMin = i;
                            argMax = i;
                            curXPixel = xpixels[i - i0];
                            pixelMinY = yval;
                            pixelMaxY = yval;
                        }
                        else
                        {
                            // scan for min and max within the pixel
                            if (yval > pixelMaxY)
                            {
                                pixelMaxY = yval;
                                argMax = i;
                            }
                            if (yval < pixelMinY)
                            {
                                pixelMinY = yval;
                                argMin = i;
                            }
                        }
                    }
                    drawIndices.Add(argMin);
                    if (argMax != argMin - i0)
                        drawIndices.Add(argMax - i0);

                    // draw the lines
                    bool smode = false;

                    if (lineMode)
                        for (int j = 0; j < drawIndices.Count - 1; ++j)
                        {
                            int idx1 = drawIndices[j];
                            int idx2 = drawIndices[j + 1];
                            double y1 = ypixels[idx1];
                            double y2 = ypixels[idx2];
                            if ((y1 < 0) || (y1 > 2500))
                                y1 = 0;
                            if ((y2 < 0) || (y2 > 2500))
                                y2 = 0;
                            if (smode)
                            {
                                g.DrawLine(temppen, (int) xpixels[idx1], (int) y1, (int) xpixels[idx2], (int) y1);
                                g.DrawLine(temppen, (int) xpixels[idx2], (int) y1, (int) xpixels[idx2], (int) y2);
                            }
                            else
                                g.DrawLine(temppen, (int) xpixels[idx1], (int) ypixels[idx1], (int) xpixels[idx2],
                                           (int) ypixels[idx2]);
                        }

                    // we put down the point markers in any case
                    for (int j = 0; j < drawIndices.Count; ++j)
                    {
                        int idx1 = drawIndices[j];
                        double y1 = ypixels[idx1];
                        PutPointMarker(g, temppen, (int) xpixels[idx1], (int) y1);
                    }
                }
            }
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            Graphics g = pe.Graphics;

            // Preliminary processing: features of data we need to plot have already been set by ScanData
            // ScanData is called by set method for XYMatrix.

            // Now we partition into sub-boxes of this box for title, x-labels, y-labels, and graph
            CreatePartition(new Rectangle(0, 0, Size.Width, Size.Height));
            RenderToGraphics(g, TitleFontSize, LabelFontSize);
        }

        #region Region Selecting Stuff

        #region Delegates

        public delegate void RegionSelectEventHandler(object source, double x1, double y1, double x2, double y2);

        #endregion

        private Point anchor;
        private bool anchored;
        private Point final;
        private bool frameDrawn;
        private Point newCorner;

        // Convert and normalize the points and draw the reversible frame.
        private void DrawReversibleRectangle(Point p1, Point p2)
        {
            var rc = new Rectangle();

            // Convert the points to screen coordinates.
            p1 = PointToScreen(p1);
            p2 = PointToScreen(p2);

            // Normalize the rectangle.
            if (p1.X < p2.X)
            {
                rc.X = p1.X;
                rc.Width = p2.X - p1.X;
            }
            else
            {
                rc.X = p2.X;
                rc.Width = p1.X - p2.X;
            }
            if (p1.Y < p2.Y)
            {
                rc.Y = p1.Y;
                rc.Height = p2.Y - p1.Y;
            }
            else
            {
                rc.Y = p2.Y;
                rc.Height = p1.Y - p2.Y;
            }

            // Draw the reversible frame.
            ControlPaint.DrawReversibleFrame(rc, Color.Black, FrameStyle.Dashed); // , Color.Blue, FrameStyle.Dashed);
        }


        private void PlotBox_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) // region select is done with left-mouse button
                return;

            anchored = true;
            anchor = new Point(e.X, e.Y);
            frameDrawn = false;
        }

        private void PlotBox_MouseMove(object sender, MouseEventArgs e)
        {
            var newPoint = new Point(e.X, e.Y);
            if (newPoint != newCorner)
                if (anchored)
                {
                    // If we have drawn previously, draw again in
                    // that spot to remove the lines.
                    if (frameDrawn)
                        DrawReversibleRectangle(anchor, newCorner);

                    // Update last point.
                    newCorner = newPoint;

                    // Draw new lines.
                    DrawReversibleRectangle(anchor, newCorner);
                    frameDrawn = true;
                }
        }

        private void PlotBox_MouseUp(object sender, MouseEventArgs e)
        {
            if (!anchored)
                return;

            final = new Point(e.X, e.Y);

            anchored = false;
            if (frameDrawn)
            {
                DrawReversibleRectangle(anchor, newCorner); // erase it
                frameDrawn = false;
            }

            if (final != anchor)
            {
                // translate pixel coordinates to other coordinates
                double x1 = XValueToPixel(anchor.X);
                double y1 = YValueToPixel(anchor.Y);
                double x2 = XValueToPixel(final.X);
                double y2 = YValueToPixel(final.Y);

                var ra = new RegionSelectArgs();
                ra.x1 = x1;
                ra.y1 = y1;
                ra.x2 = x2;
                ra.y2 = y2;
                OnRegionSelectEvent(ra);
            }
        }

        #endregion

        #region Property get/set methods

        private bool extendRight;
        private bool lineMode;
        private int markerSize = 2;
        private bool tickMode;
        private int tickResolution;

        private bool timeIsRelative;
        private bool timeMode = true;

        private string xAxisLabel = "";
        private string yAxisLabel = "";

        /// <summary>
        /// If true, regards time series as step-functions rather than connecting points with straight lines
        /// </summary>
        public bool StepMode { get; set; }

        /// <summary>
        /// if true, then times are not labelled absolutely, instead they are all
        /// relative to the minimum time
        /// </summary>
        public bool TimeIsRelative
        {
            get { return timeIsRelative; }
            set { timeIsRelative = value; }
        }

        /// <summary>
        /// True if latest point should be extended in a line to the right edge of the plot.
        /// - useful for real-time display of values.
        /// </summary>
        public bool ExtendRight
        {
            get { return extendRight; }
            set { extendRight = value; }
        }


        /// This label goes under the x-axis.
        /// </summary>
        public string XAxisLabel
        {
            get { return xAxisLabel; }
            set
            {
                xAxisLabel = value;
                Refresh();
            }
        }

        /// <summary>
        /// <summary>
        /// This label goes sideways to the left of the y-axis.
        /// </summary>
        public string YAxisLabel
        {
            get { return yAxisLabel; }
            set
            {
                yAxisLabel = value;
                Refresh();
            }
        }


        /// <summary>
        /// True if x-axis should be converted to date/times (values are tick values),  false otherwise.
        /// </summary>
        public bool TimeMode
        {
            get { return timeMode; }
            set
            {
                if (timeMode != value)
                {
                    timeMode = value;
                    Refresh();
                }
            }
        }

        /// <summary>
        /// Size of boxes put at each point
        /// </summary>
        public int MarkerSize
        {
            get { return markerSize; }
            set
            {
                markerSize = value;
                Refresh();
            }
        }

        public bool TickMode
        {
            get { return tickMode; }
            set
            {
                if (tickMode != value)
                {
                    tickMode = value;
                    Refresh();
                }
            }
        }

        /// <summary>
        /// If this is non-zero, then the y-axis is in tick units (1/32nds, etc. would be used if tickResolution==32)
        /// </summary>
        public int TickResolution
        {
            get { return tickResolution; }
            set
            {
                if (tickResolution != value)
                {
                    tickResolution = value;
                    Refresh();
                }
            }
        }

        #endregion

        #region Methods specifying values corresponding to edges of plot

        protected override double LeftXValue()
        {
            return seriesInfo[0].labelstart;
        }

        protected override double RightXValue()
        {
            return seriesInfo[0].labelend;
        }

        protected override double MinYValue()
        {
            return seriesInfo[1].labelstart;
        }

        protected override double MaxYValue()
        {
            return seriesInfo[1].labelend;
        }

        #endregion

        #region Nested type: RegionSelectArgs

        public class RegionSelectArgs : EventArgs
        {
            public double x1;
            public double x2;
            public double y1;
            public double y2;
        }

        #endregion

        #region Nested type: SingleSeriesInfo

        /// <summary>
        /// SingleSeriesInfo keeps track of x-max, x-min, y-max, y-min, 
        /// as well as the x and y bounds used in the graph.
        /// </summary>
        private class SingleSeriesInfo
        {
            private bool isempty = true;
            public double labelend;
            public double labelstart;
            public double yend;

            public double ymax, ymin;
            public double ystart;
            public double yunits;

            public double yRange()
            {
                return ymax - ymin;
            }

            public void Update(double value)
            {
                if (isempty)
                {
                    ymax = ymin = value;
                    isempty = false;
                }
                else
                {
                    if (value > ymax)
                        ymax = value;
                    if (value < ymin)
                        ymin = value;
                }
            }

            public void ComputeRecommendedUnits()
            {
                // then y-axis
                yunits = GetNiceUnitForRange(ymin, ymax, 1.0, false);
                ystart = Math.Floor(ymin/yunits)*yunits;
                yend = Math.Ceiling(ymax/yunits)*yunits;
                labelstart = ystart;
                labelend = yend;
            }
        }

        #endregion
    }
}